Skip to content

Comments

Add essential API endpoints and validation#33

Open
Sri-Krishna-V wants to merge 2 commits intomainfrom
feat/api-completions
Open

Add essential API endpoints and validation#33
Sri-Krishna-V wants to merge 2 commits intomainfrom
feat/api-completions

Conversation

@Sri-Krishna-V
Copy link
Collaborator

@Sri-Krishna-V Sri-Krishna-V commented Dec 31, 2025

  • Added health check endpoint (GET /health) for monitoring database connectivity
  • Completed Opening CRUD operations with ownership validation
  • Added application withdrawal functionality (DELETE /applications/withdraw/{id})
  • Added mark all notifications as read endpoint (PUT /notifications/read-all)
  • Added deadline and status validation to application submission
  • Added OpeningUpdate model for partial opening updates

Summary by CodeRabbit

  • New Features

    • Health check endpoint for system status verification
    • Application withdrawal functionality for students
    • Mark all notifications as read in bulk
    • Opening management endpoints: retrieve, update, and delete
    • Enhanced application workflow with deadline and status validation
  • Documentation

    • Comprehensive API documentation covering architecture, endpoints, authentication, and deployment
  • Chores

    • Added GitHub pull request template

✏️ Tip: You can customize this high-level summary in your review settings.

Implement missing CRUD operations and validation to complete core functionality:

- Add health check endpoint (GET /health) for monitoring database connectivity
- Complete Opening CRUD operations:
  * GET /openings/{id} - retrieve single opening with faculty info
  * PUT /openings/{id} - update opening (with ownership validation)
  * DELETE /openings/{id} - delete opening and related notifications
- Add OpeningUpdate model for partial updates
- Add application withdrawal (DELETE /applications/withdraw/{id})
  * Only allows withdrawing pending applications
- Add mark all notifications read (PUT /notifications/read-all)
- Add deadline validation to application submission
  * Block applications past deadline
  * Block applications to closed openings
  * Handle both date and datetime types from Neo4j

All update/delete operations include ownership validation to ensure only
the resource owner can modify their data.
@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

This PR adds documentation (GitHub PR template and expanded README), enhances the API initialization with a health check endpoint and static file serving, introduces an OpeningUpdate model for partial updates, and implements multiple new CRUD and workflow endpoints across applications, notifications, and openings routers with enhanced validation and ownership checks.

Changes

Cohort / File(s) Summary
Documentation
.github/pull_request_template.md, README.md
Added PR template with Description, Changes, and Related Issues sections; replaced README with comprehensive architecture-focused documentation covering endpoints, authentication, data models, setup, and deployment.
API Initialization
app/main.py
Added /health endpoint with database session validation; registered static file serving for uploads; contains duplicate router registrations for applications and notifications routers (potential double-mounting issue).
Data Models
app/models/openings.py
Added OpeningUpdate Pydantic model with all fields optional to support partial updates of opening records.
Application Routes
app/routers/applications.py
Added /apply/{opening_id} endpoint with deadline and status validation; added /withdraw/{opening_id} endpoint with pending status check; updated /status endpoint with aligned Cypher parameters; integrated notification creation and logging.
Notification Routes
app/routers/notifications.py
Added PUT /read-all endpoint to mark all current user notifications as read, returning affected count.
Opening Routes
app/routers/openings.py
Added GET /{opening_id} to fetch opening; added PUT /{opening_id} to update with ownership verification; added DELETE /{opening_id} to remove with authorization checks; integrated OpeningUpdate model.

Sequence Diagrams

sequenceDiagram
    actor Student
    participant API
    participant Neo4j
    
    Note over Student,Neo4j: Apply to Opening
    Student->>API: POST /apply/{opening_id}
    API->>Neo4j: Fetch opening (check deadline & status)
    Neo4j-->>API: Opening data
    alt Opening closed or deadline passed
        API-->>Student: ❌ 400 Bad Request
    else Validation passes
        API->>Neo4j: Check if already applied (APPLIED_TO)
        Neo4j-->>API: Relationship exists?
        alt Already applied
            API-->>Student: ❌ 409 Conflict
        else First application
            API->>Neo4j: Create APPLIED_TO relationship
            API->>Neo4j: Create Notification node
            Neo4j-->>API: Success
            API-->>Student: ✓ 201 Application created
        end
    end
    
    Note over Student,Neo4j: Withdraw Application
    Student->>API: DELETE /withdraw/{opening_id}
    API->>Neo4j: Validate APPLIED_TO exists & pending status
    Neo4j-->>API: Status check
    alt Status not pending
        API-->>Student: ❌ 400 Invalid status
    else Valid pending status
        API->>Neo4j: Delete APPLIED_TO relationship
        Neo4j-->>API: Success
        API-->>Student: ✓ 200 Withdrawn
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Whiskers twitch with glee,
New endpoints hop so free,
Health checks bound, and openings flow,
Applications withdraw just so,
Notifications read with delight,
The API hops through day and night! 🌙✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add essential API endpoints and validation' directly aligns with the main changes: addition of multiple API endpoints (health check, CRUD for openings, application withdrawal, notification marking) and validation logic (deadline checks, ownership validation, status validation).
✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
app/models/openings.py (1)

18-27: Consider validating the status field.

The status field accepts any string, but per the comment it should only be "Active" or "Closed". Using Literal would catch invalid values at the API boundary.

🔎 Proposed fix
 from pydantic import BaseModel
-from typing import List, Optional
+from typing import List, Literal, Optional
 from datetime import date

 class OpeningUpdate(BaseModel):
     """All fields optional for partial updates."""
     title: Optional[str] = None
     description: Optional[str] = None
     required_skills: Optional[List[str]] = None
     expected_duration: Optional[str] = None
     target_years: Optional[List[str]] = None
     min_cgpa: Optional[float] = None
     deadline: Optional[date] = None
-    status: Optional[str] = None  # "Active" or "Closed"
+    status: Optional[Literal["Active", "Closed"]] = None
README.md (2)

105-112: Add language specifier to fenced code block.

Per linting, this code block should have a language specifier. Since it's pseudo-code/formula, use text or plaintext.

🔎 Proposed fix
-    ```
+    ```text
     Base Score = (Matched Skills / Total Required) × 100

434-471: Add language specifier to project structure block.

The directory tree should have a language specifier for consistent linting.

🔎 Proposed fix
-```
+```text
 gurusetu-backend/
 ├── app/
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbac071 and 465a0a5.

📒 Files selected for processing (7)
  • .github/pull_request_template.md
  • README.md
  • app/main.py
  • app/models/openings.py
  • app/routers/applications.py
  • app/routers/notifications.py
  • app/routers/openings.py
🧰 Additional context used
🧬 Code graph analysis (4)
app/routers/openings.py (3)
app/models/openings.py (2)
  • OpeningCreate (6-15)
  • OpeningUpdate (18-27)
app/core/security.py (1)
  • get_current_user (58-77)
app/core/database.py (2)
  • get_session (33-42)
  • close (28-31)
app/routers/applications.py (2)
app/core/security.py (1)
  • get_current_user (58-77)
app/core/database.py (2)
  • get_session (33-42)
  • close (28-31)
app/main.py (1)
app/core/database.py (2)
  • get_session (33-42)
  • close (28-31)
app/routers/notifications.py (2)
app/core/database.py (2)
  • close (28-31)
  • get_session (33-42)
app/core/security.py (1)
  • get_current_user (58-77)
🪛 markdownlint-cli2 (0.18.1)
README.md

105-105: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


434-434: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.10)
app/routers/openings.py

37-37: Abstract raise to an inner function

(TRY301)


42-42: Do not catch blind exception: Exception

(BLE001)


43-43: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


44-44: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


53-53: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


68-69: Abstract raise to an inner function

(TRY301)


74-74: Abstract raise to an inner function

(TRY301)


103-103: Consider moving this statement to an else block

(TRY300)


106-106: Do not catch blind exception: Exception

(BLE001)


107-107: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


108-108: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


116-116: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


137-138: Abstract raise to an inner function

(TRY301)


140-140: Consider moving this statement to an else block

(TRY300)


143-143: Do not catch blind exception: Exception

(BLE001)


144-144: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


145-145: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


153-153: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

app/routers/applications.py

22-22: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


40-41: Abstract raise to an inner function

(TRY301)


44-47: Abstract raise to an inner function

(TRY301)


61-61: Do not catch blind exception: Exception

(BLE001)


62-62: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


63-64: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


88-88: Abstract raise to an inner function

(TRY301)


92-93: Abstract raise to an inner function

(TRY301)


104-105: Abstract raise to an inner function

(TRY301)


114-115: Abstract raise to an inner function

(TRY301)


151-151: Consider moving this statement to an else block

(TRY300)


153-153: Do not catch blind exception: Exception

(BLE001)


155-156: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


212-212: Consider moving this statement to an else block

(TRY300)


214-214: Do not catch blind exception: Exception

(BLE001)


216-216: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

app/main.py

67-67: Consider moving this statement to an else block

(TRY300)


68-68: Do not catch blind exception: Exception

(BLE001)

app/routers/notifications.py

51-51: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🔇 Additional comments (5)
.github/pull_request_template.md (1)

1-13: LGTM!

Clean and minimal PR template with essential sections for description, changes, and related issues.

app/routers/notifications.py (1)

50-65: LGTM!

The mark_all_notifications_read endpoint correctly filters by user ownership, only updates unread notifications, and properly manages the database session.

app/routers/openings.py (1)

12-46: Verify that public access to openings is intended.

The get_opening endpoint has no authentication requirement, meaning anyone can view opening details including faculty information. If this is intentional for public browsing, this is fine. Otherwise, consider adding Depends(get_current_user).

app/routers/applications.py (2)

21-66: LGTM!

The withdraw_application endpoint correctly validates student role, checks pending status before allowing withdrawal, and properly re-raises HTTPException before catching generic exceptions.


96-105: Good handling of Neo4j date types.

The deadline validation correctly handles both Neo4j native date types (via to_native()) and Python datetime/date objects. This prevents type-related runtime errors when comparing dates.

Comment on lines +60 to +72
@app.get("/health")
def health_check():
"""Health check endpoint for monitoring and load balancers."""
session = None
try:
session = db.get_session()
session.run("RETURN 1")
return {"status": "healthy", "database": "connected"}
except Exception as e:
return {"status": "unhealthy", "database": "disconnected", "error": str(e)}
finally:
if session:
session.close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Health check should return HTTP 503 when unhealthy.

Load balancers and monitoring tools typically rely on HTTP status codes, not response body content. Returning HTTP 200 with "status": "unhealthy" will cause the service to appear healthy even when the database is down.

🔎 Proposed fix
 @app.get("/health")
 def health_check():
     """Health check endpoint for monitoring and load balancers."""
     session = None
     try:
         session = db.get_session()
         session.run("RETURN 1")
         return {"status": "healthy", "database": "connected"}
     except Exception as e:
-        return {"status": "unhealthy", "database": "disconnected", "error": str(e)}
+        raise HTTPException(
+            status_code=503,
+            detail={"status": "unhealthy", "database": "disconnected", "error": str(e)}
+        )
     finally:
         if session:
             session.close()

Note: You'll need to import HTTPException from fastapi at the top of the file.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.10)

67-67: Consider moving this statement to an else block

(TRY300)


68-68: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In app/main.py around lines 60 to 72, the health_check endpoint currently
returns an HTTP 200 body even when the database is down; change it to raise
fastapi.HTTPException(status_code=503, detail=...) when the DB check fails so
clients/load‑balancers receive a 503 status; ensure you import HTTPException
from fastapi at the top, keep the successful path returning a 200 response, and
include the error message in the exception detail (and still close the session
in finally).

Comment on lines 153 to +156
except Exception as e:
print(f"Application Error: {e}")
raise HTTPException(status_code=500, detail="Server error processing application")
raise HTTPException(
status_code=500, detail="Server error processing application")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

HTTPException is swallowed and replaced with a generic 500 error.

The except Exception block catches HTTPException raised earlier (lines 88, 92-93, 104-105, 114-115), losing the specific error details. User-facing validation errors (400, 404) will incorrectly return as 500 errors.

🔎 Proposed fix
         return {"message": "Application submitted successfully"}

+    except HTTPException:
+        raise
     except Exception as e:
         print(f"Application Error: {e}")
         raise HTTPException(
             status_code=500, detail="Server error processing application")
🧰 Tools
🪛 Ruff (0.14.10)

153-153: Do not catch blind exception: Exception

(BLE001)


155-156: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
In app/routers/applications.py around lines 153 to 156, the broad except
Exception catches and converts previously raised HTTPException instances into a
generic 500, losing original status and detail; change the handler to re-raise
HTTPException unchanged (e.g., check isinstance(e, HTTPException) and raise) or
add a separate except HTTPException: raise block above the generic except, and
only handle/log non-HTTP exceptions before raising a new 500 with a concise
message.

Comment on lines 214 to 216
except Exception as e:
print(f"Status Update Error: {e}")
raise HTTPException(status_code=500, detail="Failed to update status")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same issue: HTTPException is swallowed in update_application_status.

The generic except Exception catches the HTTPException from line 167, causing the "Access denied" 403 error to become a 500 error.

🔎 Proposed fix
         return {"message": f"Applicant marked as {data.status}"}

+    except HTTPException:
+        raise
     except Exception as e:
         print(f"Status Update Error: {e}")
         raise HTTPException(status_code=500, detail="Failed to update status")
🧰 Tools
🪛 Ruff (0.14.10)

214-214: Do not catch blind exception: Exception

(BLE001)


216-216: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
In app/routers/applications.py around lines 214-216, the broad "except Exception
as e" block is catching and converting an already-raised HTTPException (e.g.,
the 403 "Access denied") into a 500; modify the error handling so HTTPException
instances are not swallowed: first check for and re-raise HTTPException (or add
an except HTTPException: raise) and then handle other exceptions (log the error)
and raise a new HTTPException(status_code=500, detail="Failed to update status")
for unexpected errors. Ensure you remove or replace the print with proper
logging if present.

Comment on lines +126 to +138
delete_query = """
MATCH (f:User {user_id: $faculty_id})-[:POSTED]->(o:Opening {id: $opening_id})
OPTIONAL MATCH (o)-[r]-() // All relationships
OPTIONAL MATCH (n:Notification {trigger_id: $opening_id})
DETACH DELETE o, n
RETURN count(o) AS deleted
"""
result = session.run(
delete_query, faculty_id=current_user["user_id"], opening_id=opening_id).single()

if not result or result["deleted"] == 0:
raise HTTPException(
status_code=404, detail="Opening not found or you don't have permission")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

count(o) after DETACH DELETE may not work as expected.

After DETACH DELETE o, the variable o is unbound, so count(o) may return 0 or cause issues depending on Neo4j version. Consider capturing a marker before deletion.

🔎 Proposed fix
         delete_query = """
         MATCH (f:User {user_id: $faculty_id})-[:POSTED]->(o:Opening {id: $opening_id})
+        WITH o, o.id AS matched_id
         OPTIONAL MATCH (o)-[r]-()  // All relationships
         OPTIONAL MATCH (n:Notification {trigger_id: $opening_id})
         DETACH DELETE o, n
-        RETURN count(o) AS deleted
+        RETURN matched_id IS NOT NULL AS deleted
         """
         result = session.run(
             delete_query, faculty_id=current_user["user_id"], opening_id=opening_id).single()

-        if not result or result["deleted"] == 0:
+        if not result or not result["deleted"]:
             raise HTTPException(
                 status_code=404, detail="Opening not found or you don't have permission")
🧰 Tools
🪛 Ruff (0.14.10)

137-138: Abstract raise to an inner function

(TRY301)

🤖 Prompt for AI Agents
In app/routers/openings.py around lines 126 to 138, the Cypher uses count(o)
after DETACH DELETE which is unsafe because o is unbound after deletion; change
the query to capture a marker/count before deleting (for example use WITH to
compute/count o or collect a marker into a variable, then DETACH DELETE using
that variable, and finally RETURN the previously computed count), and keep the
Python call signature the same so the result["deleted"] reflects the number
deleted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant